﻿class Workflow {
    constructor(workflowEditorEleId, dotNetHelper) {
        this.dotNetHelper = dotNetHelper;
        this.workflowEditorEle = document.getElementById(workflowEditorEleId);
        this.workflowViewportEle = this.workflowEditorEle.querySelector(".workflow-viewport");
        this.workflowCanvasEle = this.workflowEditorEle.querySelector('.workflow-viewport > .workflow-canvas');
        this.workflowCanvasTransform = this.workflowCanvasEle.style.transform;
        this.currentContainerId = this.workflowCanvasEle.id;
        this.containerOffsetSet = {};
        this.workflowViewportEle.addEventListener('mousewheel', (e) => this.middleMouseScrollAndZoom(e));
        this.workflowViewportEle.onpointerdown = (e) => {
            if (e.target.nodeName !== 'INPUT' && e.target.nodeName !== 'TEXTAREA') {
                let x = 0, y = 0, firstMove = true;
                this.workflowViewportEle.onpointermove = (e) => {
                    if (e.buttons !== 1) {
                        this.workflowViewportEle.onpointermove = null;
                        this.workflowViewportEle.releasePointerCapture(e.pointerId);
                        return;
                    }
                    if (Math.abs(x) > 5 || Math.abs(y) > 5) {
                        this.pan(e.movementX, e.movementY);
                        if (firstMove) {
                            this.workflowViewportEle.style.cursor = 'grabbing';
                            this.workflowViewportEle.setPointerCapture(e.pointerId);
                            firstMove = false;
                        }
                    } else {
                        x += e.movementX;
                        y += e.movementY;
                    }
                };
            }
        };
        this.workflowViewportEle.onpointerup = (e) => {
            this.workflowViewportEle.onpointermove = null;
            this.workflowViewportEle.releasePointerCapture(e.pointerId);
            this.workflowViewportEle.style.cursor = null;
        };
        this.startAnimationFrame();
        this.bestFit();
    }

    keepLocation() {
        let rematch = false;
        if (parseInt(this.workflowEditorEle.offsetWidth) !== this.workflowEditorEleWidth) {
            this.workflowEditorEleWidth = parseInt(this.workflowEditorEle.offsetWidth);
            rematch = true;
        }
        if (parseInt(this.workflowEditorEle.offsetHeight) !== this.workflowEditorEleHeight) {
            this.workflowEditorEleHeight = parseInt(this.workflowEditorEle.offsetHeight);
            rematch = true;
        }
        if (parseInt(this.workflowCanvasEle.offsetWidth) !== this.workflowCanvasEleWidth) {
            this.workflowCanvasEleWidth = parseInt(this.workflowCanvasEle.offsetWidth);
            rematch = true;
        }
        if (parseInt(this.workflowCanvasEle.offsetHeight) !== this.workflowCanvasEleHeight) {
            this.workflowCanvasEleHeight = parseInt(this.workflowCanvasEle.offsetHeight);
            rematch = true;
        }
        if (rematch) {
            const containerStartNodeEle = this.workflowEditorEle.querySelector('.workflow-canvas > .start-node');
            if (this.containerStartNodeOffsetX !== undefined) {
                const canvasBounds = this.getCanvasBounds();
                const newOffsetX = containerStartNodeEle.offsetLeft;
                const newLeft = canvasBounds.left - (newOffsetX - this.containerStartNodeOffsetX);
                this.setCanvasLocation(newLeft, canvasBounds.top, true);
                this.containerStartNodeOffsetX = newOffsetX;
            } else {
                this.containerStartNodeOffsetX = containerStartNodeEle.offsetLeft;
            }
        }
        if (!this.isDispose) {
            this.keepLocationFrameHandle = window.requestAnimationFrame(() => this.keepLocation());
        }
    }

    resetContainerStartNodeOffsetX() {
        const containerStartNodeEle = this.workflowEditorEle.querySelector('.workflow-canvas > .start-node');
        this.containerStartNodeOffsetX = containerStartNodeEle.offsetLeft;
    }

    setCanvasTransform() {
        if (this.workflowCanvasTransform && this.workflowCanvasTransform !== this.workflowCanvasEle.style.transform) {
            this.workflowCanvasEle.style.transform = this.workflowCanvasTransform;
        }
        if (!this.isDispose) {
            this.setCanvasTransformFrameHandle = window.requestAnimationFrame(() => this.setCanvasTransform());
        }
    }

    stopAnimationFrame() {
        this.containerStartNodeOffsetX = undefined;
        this.workflowCanvasTransform = undefined;
        if (this.keepLocationFrameHandle) {
            window.cancelAnimationFrame(this.keepLocationFrameHandle);
        }
        if (this.setCanvasTransformFrameHandle) {
            window.cancelAnimationFrame(this.setCanvasTransformFrameHandle);
        }
    }

    startAnimationFrame() {
        this.keepLocation();
        this.setCanvasTransform();
    }

    getCanvasScale() {
        let scale = 1;
        const transform = this.workflowCanvasTransform;
        if (transform) {
            const index = transform.indexOf('scale(');
            if (index > -1) {
                scale = parseFloat(transform.substring(index + 6, transform.indexOf(')', index)));
            }
        }
        return scale;
    }

    getCanvasBounds() {
        let left = 0, top = 0;
        const transform = this.workflowCanvasTransform;
        if (transform) {
            const index = transform.indexOf('translate(');
            if (index > -1) {
                const offset = transform.substring(index + 10, transform.indexOf(')', index));
                left = parseFloat(offset.split(',')[0]);
                top = parseFloat(offset.split(',')[1]);
            }
        }
        return { left, top, width: this.workflowCanvasEle.offsetWidth, height: this.workflowCanvasEle.offsetHeight };
    }

    setCanvasLocation(left, top, force) {
        this.setCanvasLocationAndScale(left, top, this.getCanvasScale(), force);
    }

    setCanvasLocationAndScale(left, top, scale, force) {
        this.workflowCanvasTransform = `scale(${scale}) translate(${left}px, ${top}px)`;
        if (this.currentContainerId === this.workflowCanvasEle.id) {
            this.containerOffsetSet[this.currentContainerId] = { left, top, scale };
        }
        if (force) {
            this.workflowCanvasEle.style.transform = this.workflowCanvasTransform;
        }
    }

    getCanvasScaleBounds() {
        const scale = this.getCanvasScale();
        const bounds = this.getCanvasBounds();
        return {
            left: bounds.left,
            top: bounds.top,
            width: bounds.width * scale,
            height: bounds.height * scale
        };
    }

    getViewportSize() {
        return {
            width: this.workflowViewportEle.offsetWidth,
            height: this.workflowViewportEle.offsetHeight
        };
    }

    viewportPointToCanvans(point) {
        const canvasBounds = this.getCanvasBounds();
        const canvasScale = this.getCanvasScale();
        return {
            x: point.x / canvasScale - canvasBounds.left,
            y: point.y / canvasScale - canvasBounds.top
        };
    }

    pan(offsetX, offsetY) {
        const scale = this.getCanvasScale();
        const bounds = this.getCanvasBounds();
        const left = bounds.left + offsetX / scale;
        const top = bounds.top + offsetY / scale;
        this.setCanvasLocation(left, top);
    }

    zoomTo(scale, viewportPoint) {
        const viewportSize = this.getViewportSize();
        viewportPoint = viewportPoint ?? {
            x: viewportSize.width / 2,
            y: viewportSize.height / 2
        };
        const canvasPoint = this.viewportPointToCanvans(viewportPoint);
        const left = viewportPoint.x - canvasPoint.x * scale;
        const top = viewportPoint.y - canvasPoint.y * scale;
        this.setCanvasLocationAndScale(left / scale, top / scale, scale);
        this.dotNetHelper.invokeMethodAsync('SetZoomAsync', scale);
    }

    middleMouseScrollAndZoom(e) {
        let scale = this.getCanvasScale();
        if (e.ctrlKey) {
            if (e.wheelDeltaY > 0) {
                scale += 0.1;
            } else {
                scale -= 0.1;
            }
            scale = Math.max(0.1, scale);
            scale = Math.min(1.5, scale);
            this.zoomTo(scale);
        } else {
            let { left, top } = this.getCanvasBounds();
            if (e.wheelDeltaY > 0) {
                top += 50 / scale;
            } else {
                top -= 50 / scale;
            }
            this.setCanvasLocationAndScale(left, top, scale);
        }
        e.preventDefault();
        e.stopPropagation();
    };

    bestFit() {
        this.containerStartNodeOffsetX = undefined;
        const currentContainerId = this.workflowCanvasEle.id;
        if (currentContainerId !== this.currentContainerId) {
            this.currentContainerId = currentContainerId;
            if (this.containerOffsetSet[currentContainerId]) {
                const { left, top, scale } = this.containerOffsetSet[currentContainerId];
                this.setCanvasLocationAndScale(left, top, scale, true);
                this.startAnimationFrame();
                return scale;
            }
        }
        const viewportSize = this.getViewportSize();
        const canvasBounds = this.getCanvasBounds();
        let scale = viewportSize.width / canvasBounds.width;
        scale = Math.min(1, scale);
        scale = Math.max(0.5, scale);
        const width = canvasBounds.width * scale;
        const height = canvasBounds.height * scale;
        const left = (viewportSize.width - width) / 2;
        let top = 0;
        if (viewportSize.height > height) {
            top = (viewportSize.height - height) / 2
        }
        this.setCanvasLocationAndScale(left / scale, top / scale, scale, true);
        this.resetContainerStartNodeOffsetX();
        this.dotNetHelper.invokeMethodAsync('SetZoomAsync', scale);
        return scale;
    }

    fitToDrawing() {
        this.containerStartNodeOffsetX = undefined;
        const viewportSize = this.getViewportSize();
        const canvasBounds = this.getCanvasBounds();
        let scale = 1;
        if (viewportSize.width / viewportSize.height > canvasBounds.width / canvasBounds.height) {
            scale = viewportSize.height / canvasBounds.height;
        } else {
            scale = viewportSize.width / canvasBounds.width;
        }
        const width = canvasBounds.width * scale;
        const height = canvasBounds.height * scale;
        const left = (viewportSize.width - width) / 2;
        const top = (viewportSize.height - height) / 2
        this.setCanvasLocationAndScale(left / scale, top / scale, scale, true);
        this.resetContainerStartNodeOffsetX();
        this.dotNetHelper.invokeMethodAsync('SetZoomAsync', scale);
        return scale;
    }

    dispose() {
        this.stopAnimationFrame();
        this.workflowViewportEle.onpointerdown = null;
        this.workflowViewportEle.onpointermove = null;
        this.workflowViewportEle.onpointerup = null;
        this.workflowViewportEle.removeEventListener('mousewheel', this.middleMouseZoomHandle);
        this.isDispose = true;
    }
}

export function init(workflowEleId, dotNetHelper) {
    const ele = document.getElementById(workflowEleId);
    if (ele) {
        ele.workflow = new Workflow(workflowEleId, dotNetHelper);
    }
}

export function zoomTo(workflowEleId, scale) {
    const ele = document.getElementById(workflowEleId);
    if (ele?.workflow) {
        const value = ele.workflow.zoomTo(scale);
        ele?.navigator?.layout();
        return value;
    }
}

export function bestFit(workflowEleId) {
    const ele = document.getElementById(workflowEleId);
    if (ele?.workflow) {
        const zoom = ele.workflow.bestFit();
        ele?.navigator?.layout();
        return zoom;
    }
}

export function fitToDrawing(workflowEleId) {
    const ele = document.getElementById(workflowEleId);
    if (ele?.workflow) {
        const zoom = ele.workflow.fitToDrawing();
        ele?.navigator?.layout();
        return zoom;
    }
}

export function dispose(workflowEleId) {
    const ele = document.getElementById(workflowEleId);
    if (ele?.workflow) {
        ele.workflow.dispose();
    }
}
